import * as step from '@flow-step/step-toolkit'
import * as sinon from 'sinon';
import {expect} from 'chai';
import proxyquire from 'proxyquire';
// @ts-ignore
import {
    formatDate,
    getTagFromImageId,
    convertToVpcImage,
    getImageRepositoryFromImageId,
    getDockerBuildArgs, parseImage
} from '../src/toolkit'
import child_process from 'child_process'

describe('checkDockerInstallation', () => {
    let execSyncStub: sinon.SinonStub;
    let addPathStub: sinon.SinonStub;
    let getPlatformStub: sinon.SinonStub;

    beforeEach(() => {
        execSyncStub = sinon.stub(child_process, 'execSync');
        addPathStub = sinon.stub(step, 'addPath');
        getPlatformStub = sinon.stub(step.platform, 'getPlatform');
    });

    afterEach(() => {
        execSyncStub.restore();
        addPathStub.restore();
        getPlatformStub.restore();
    });

    it('should return true when docker and docker buildx are installed correctly', () => {
        getPlatformStub.returns(step.platform.PLATFORM_LINUX);
        
        execSyncStub.onFirstCall().returns(Buffer.from("Docker version 27.2.0, build 3ab4256"));
        execSyncStub.onSecondCall().returns(Buffer.from('github.com/docker/buildx v0.16.2-desktop.1 081c21b9e461293ae243a1ff813a680a4f5f8fb9'));

        const toolkitProxy = proxyquire('../src/toolkit', {
            'child_process': {execSync: execSyncStub},
            '@flow-step/step-toolkit': {addPath: addPathStub, getPlatformStub: getPlatformStub}
        });

        const result = toolkitProxy.checkDockerInstallation();

        expect(result).to.be.true;
        expect(execSyncStub.callCount).to.equal(2);
    });

    it('should return false when docker is not installed', () => {
        getPlatformStub.returns(step.platform.PLATFORM_LINUX);
        execSyncStub.throws(new Error('docker: command not found'));

        const toolkitProxy = proxyquire('../src/toolkit', {
            'child_process': {execSync: execSyncStub},
            '@flow-step/step-toolkit': {addPath: addPathStub, getPlatformStub: getPlatformStub}
        });
        const result = toolkitProxy.checkDockerInstallation();

        expect(result).to.be.false;
        expect(execSyncStub.callCount).to.equal(1);
    });

    it('should return false when docker buildx is not installed', () => {
        getPlatformStub.returns(step.platform.PLATFORM_LINUX);
        execSyncStub.onFirstCall().returns(Buffer.from('Docker version 27.2.0, build 3ab4256'));
        execSyncStub.onSecondCall().throws(new Error('docker buildx: command not found'));

        const toolkitProxy = proxyquire('../src/toolkit', {
            'child_process': {execSync: execSyncStub},
            '@flow-step/step-toolkit': {addPath: addPathStub, getPlatformStub: getPlatformStub}
        });
        const result = toolkitProxy.checkDockerInstallation();

        expect(result).to.be.false;
        expect(execSyncStub.callCount).to.equal(2);
    });

    it('should return false when docker buildx version is not compatible', () => {
        getPlatformStub.returns(step.platform.PLATFORM_LINUX);
        execSyncStub.onFirstCall().returns(Buffer.from('Docker version 27.2.0, build 3ab4256'));
        execSyncStub.onSecondCall().returns(Buffer.from('docker buildx version 0.1.0'));

        const toolkitProxy = proxyquire('../src/toolkit', {
            'child_process': {execSync: execSyncStub},
            '@flow-step/step-toolkit': {addPath: addPathStub, getPlatformStub: getPlatformStub}
        });
        const result = toolkitProxy.checkDockerInstallation();

        expect(result).to.be.false;
        expect(execSyncStub.callCount).to.equal(2);
    });
});

describe('waitDockerDaemonReady', function () {
    let execSyncStub: sinon.SinonStub;
    let infoStub: sinon.SinonStub;

    beforeEach(function () {
        execSyncStub = sinon.stub();
        infoStub = sinon.stub(step, 'info');
    });

    afterEach(function () {
        infoStub.restore();
    });

    it('should wait until Docker daemon is ready', async function () {
        execSyncStub.onFirstCall().throws(new Error('Command failed'));
        execSyncStub.onSecondCall().returns(null);

        const waitDockerDaemonReadyStubbed = proxyquire('../src/toolkit', {
            'child_process': {execSync: execSyncStub}
        }).waitDockerDaemonReady;

        await waitDockerDaemonReadyStubbed();

        expect(execSyncStub.callCount).to.equal(2);
        sinon.assert.calledWith(infoStub, 'Docker daemon not ready, waiting...');
        sinon.assert.calledWith(infoStub, 'Docker daemon is ready');
    });

    it('should exit if Docker daemon is already ready', async function () {
        execSyncStub.returns(null);

        const waitDockerDaemonReadyStubbed = proxyquire('../src/toolkit', {
            'child_process': {execSync: execSyncStub}
        }).waitDockerDaemonReady;

        await waitDockerDaemonReadyStubbed();

        expect(execSyncStub.callCount).to.equal(1);
        sinon.assert.neverCalledWith(infoStub, 'Docker daemon not ready, waiting...');
        sinon.assert.calledWith(infoStub, 'Docker daemon is ready');
    });
});

describe('createDockerBuildxBuilder', () => {
    let execSyncStub: sinon.SinonStub;

    beforeEach(() => {
        execSyncStub = sinon.stub();
    });

    afterEach(() => {
        execSyncStub.reset();
    });

    it('should not attempt to create a new builder if one already exists', () => {
        execSyncStub.onFirstCall().returns("");

        const createDockerBuildxBuilderStubbed = proxyquire('../src/toolkit', {
            'child_process': {execSync: execSyncStub}
        }).createDockerBuildxBuilder;

        createDockerBuildxBuilderStubbed();

        expect(execSyncStub.callCount).to.equal(1);
        sinon.assert.calledWith(execSyncStub, 'docker buildx inspect flow-build-container > /dev/null 2>&1');

    });

    it('should create a new builder if one does not exist', () => {
        execSyncStub.onFirstCall().throws(new Error('Command failed'));
        execSyncStub.onSecondCall().returns("");

        const createDockerBuildxBuilderStubbed = proxyquire('../src/toolkit', {
            'child_process': {execSync: execSyncStub}
        }).createDockerBuildxBuilder;

        createDockerBuildxBuilderStubbed();

        expect(execSyncStub.callCount).to.equal(2);
        sinon.assert.calledWith(execSyncStub, 'docker buildx inspect flow-build-container > /dev/null 2>&1');
        let arch: string = step.platform.getArch()
        if (arch === 'x64') {
            sinon.assert.calledWith(execSyncStub, 'docker buildx create --name flow-build-container --use --bootstrap --driver=docker-container --driver-opt=image=build-steps-public-registry.cn-beijing.cr.aliyuncs.com/build-steps/docker@sha256:c3cb08891c15763d426ba017fa7ce957537a779837f5bf2f7b50de3796e13918,network=host')
        } else if (arch === 'arm64') {
            sinon.assert.calledWith(execSyncStub, 'docker buildx create --name flow-build-container --use --bootstrap --driver=docker-container --driver-opt=image=build-steps-public-registry.cn-beijing.cr.aliyuncs.com/build-steps/docker@sha256:957eb319e91a8f4da122e988fc54632c929c94085ec0590ff47c77ad29a8e33f,network=host')
        }
    });
});

describe('formatContextPath', () => {
    let formatContextPath: (contextPath: string | undefined, dockerfilePath: string, projectDir: string) => string;
    let isAbsoluteStub: sinon.SinonStub;
    let dirnameStub: sinon.SinonStub;
    let joinStub: sinon.SinonStub;

    before(() => {
        isAbsoluteStub = sinon.stub();
        dirnameStub = sinon.stub();
        joinStub = sinon.stub();

        const module = proxyquire('../src/toolkit', {
            'path': {
                isAbsolute: isAbsoluteStub,
                dirname: dirnameStub,
                join: joinStub,
            },
        });

        formatContextPath = module.formatContextPath;
    });

    it('should use the projectDir as context path when contextPath is undefined and dockerfilePath has no slashes', () => {
        const contextPath = undefined;
        const dockerfilePath = 'Dockerfile';
        const projectDir = '/my/project/dir';
        const result = formatContextPath(contextPath, dockerfilePath, projectDir);
        expect(result).to.equal(projectDir);
    });

    it('should use the dirname of dockerfilePath when dockerfilePath is absolute', () => {
        const contextPath = undefined;
        const dockerfilePath = '/absolute/path/to/Dockerfile';
        const projectDir = '/my/project/dir';

        isAbsoluteStub.withArgs(dockerfilePath).returns(true);
        dirnameStub.withArgs(dockerfilePath).returns('/absolute/path/to');
        const result = formatContextPath(contextPath, dockerfilePath, projectDir);

        sinon.assert.calledWith(isAbsoluteStub, dockerfilePath);
        sinon.assert.calledWith(dirnameStub, dockerfilePath);
        expect(result).to.equal('/absolute/path/to');
    });

    it('should join projectDir with contextPath when contextPath is defined', () => {
        const contextPath = 'my/context';
        const dockerfilePath = 'Dockerfile';
        const projectDir = '/my/project/dir';
        joinStub.withArgs(projectDir, contextPath).returns('/my/project/dir/my/context');
        const result = formatContextPath(contextPath, dockerfilePath, projectDir);
        sinon.assert.calledWith(joinStub, projectDir, contextPath);
        expect(result).to.equal('/my/project/dir/my/context');
    });

    it('should join projectDir with dirname of dockerfilePath when dockerfilePath contains slashes and is not absolute', () => {
        const contextPath = undefined;
        const dockerfilePath = 'path/to/Dockerfile';
        const projectDir = '/my/project/dir';
        isAbsoluteStub.withArgs(dockerfilePath).returns(false);
        dirnameStub.withArgs(dockerfilePath).returns('path/to');
        joinStub.withArgs(projectDir, 'path/to').returns('/my/project/dir/path/to');

        const result = formatContextPath(contextPath, dockerfilePath, projectDir);
        sinon.assert.calledWith(isAbsoluteStub, dockerfilePath);
        sinon.assert.calledWith(dirnameStub, dockerfilePath);
        sinon.assert.calledWith(joinStub, projectDir, 'path/to');
        expect(result).to.equal('/my/project/dir/path/to');
    });

    it('should join projectDir contextPath if contextPath is not empty, regardless of dockerfilePath', () => {
        const contextPath = '/absolute/context';
        const dockerfilePath = './Dockerfile';
        const projectDir = '/my/project/dir';
        isAbsoluteStub.withArgs(dockerfilePath).returns(false);
        joinStub.withArgs(projectDir, contextPath).returns('/my/project/dir/absolute/context');

        const result = formatContextPath(contextPath, dockerfilePath, projectDir);
        sinon.assert.calledWith(joinStub, projectDir, contextPath);
        expect(result).to.equal('/my/project/dir/absolute/context');
    });

    after(() => {
        isAbsoluteStub.reset()
        dirnameStub.reset();
        joinStub.reset();
    });
});


describe('formatDate', () => {
    it('should format the date correctly', () => {
        const date = new Date(2022, 3, 14, 10, 30, 45);
        const formattedDate = formatDate(date);
        expect(formattedDate).to.equal('2022-04-14-10-30-45');
    });
});

describe('getImageRepositoryFromImageId', () => {
    it('should return the repository when imageId does not has tag and registry port', () => {
        const imageId = 'example.cr.aliyuncs.com/namespace/repo';
        const result = getImageRepositoryFromImageId(imageId);
        expect(result).to.equal('example.cr.aliyuncs.com/namespace/repo');
    });

    it('should return the repository when imageId has tag without registry port', () => {
        const imageId = 'example.cr.aliyuncs.com/namespace/repo:tag';
        const result = getImageRepositoryFromImageId(imageId);
        expect(result).to.equal('example.cr.aliyuncs.com/namespace/repo');
    });

    it('should return the repository when imageId has tag with registry port', () => {
        const imageId = 'example.cr.aliyuncs.com:5000/namespace/repo:tag';
        const result = getImageRepositoryFromImageId(imageId);
        expect(result).to.equal('example.cr.aliyuncs.com:5000/namespace/repo');
    });

    it('should return the repository when imageId has registry port without tag', () => {
        const imageId = 'example.cr.aliyuncs.com:5000/namespace/repo';
        const result = getImageRepositoryFromImageId(imageId);
        expect(result).to.equal('example.cr.aliyuncs.com:5000/namespace/repo');
    });
});

describe('getTagFromImageId', () => {
    it('should return the tag from imageId with format "registry-host:port/namespace/image-name:tag"', () => {
        const imageId = 'registry-host:port/namespace/image-name:tag';
        const result = getTagFromImageId(imageId);
        expect(result).to.equal('tag');
    });

    it('should return the tag from imageId with format "registry-host/namespace/image-name:tag"', () => {
        const imageId = 'registry-host/namespace/image-name:tag';
        const result = getTagFromImageId(imageId);
        expect(result).to.equal('tag');
    });

    it('should return empty string from imageId with format "registry-host:port/namespace/image-name"', () => {
        const imageId = 'registry-host:port/namespace/image-name';
        const result = getTagFromImageId(imageId);
        expect(result).to.equal('');
    });

    it('should return empty string from imageId with format "registry-host/namespace/image-name"', () => {
        const imageId = 'registry-host/namespace/image-name';
        const result = getTagFromImageId(imageId);
        expect(result).to.equal('');
    });


    it('should return empty string if imageId is empty', () => {
        const imageId = '';
        const result = getTagFromImageId(imageId);
        expect(result).to.equal('');
    });
});

describe('addVpcToImage', () => {
    it('should add -vpc to the domain when image url contains cr.aliyuncs.com', () => {
        const image = 'example.cr.aliyuncs.com/namespace/repo:tag';
        const result = convertToVpcImage(image);
        expect(result).to.equal('example-vpc.cr.aliyuncs.com/namespace/repo:tag');
    });

    it('should add -vpc to the domain when image url contains aliyuncs.com', () => {
        const image = 'example.aliyuncs.com/namespace/repo:tag';
        const result = convertToVpcImage(image);
        expect(result).to.equal('example-vpc.aliyuncs.com/namespace/repo:tag');
    });

    it('should not add -vpc to the domain when image url does not contain aliyuncs.com', () => {
        const image = 'example.com/namespace/repo:tag';
        const result = convertToVpcImage(image);
        expect(result).to.equal(image);
    });

    it('should not add -vpc to the domain when image url contain -vpc and aliyuncs.com', () => {
        const image = 'example-vpc.aliyuncs.com/namespace/repo:tag';
        const result = convertToVpcImage(image);
        expect(result).to.equal(image);
    });

    it('should return the original image when image is empty', () => {
        const image = '';
        const result = convertToVpcImage(image);
        expect(result).to.equal(image);
    });
});

describe('getDockerBuildArgs', () => {
    it('should return the correct args when useCache is false', () => {
        const image = 'test/image:latest';
        const targetDockerfilePath = 'Dockerfile';
        const contextPath = '.';
        const result = getDockerBuildArgs(image, targetDockerfilePath, contextPath, "no-cache", "");

        expect(result.toString()).to.equal([
            'buildx',
            'build',
            '--progress=plain',
            '-t',
            image,
            '-f',
            targetDockerfilePath,
            contextPath,
            '--push',
            '--no-cache',
        ].toString());
    });

    it('should return the correct args when cacheType is remote and cacheImageId is provided', () => {
        const image = 'test/image:latest';
        const targetDockerfilePath = 'Dockerfile';
        const contextPath = '.';
        const result = getDockerBuildArgs(image, targetDockerfilePath, contextPath, "remote", "remote/cache/image:latest");

        expect(result.toString()).to.equal([
            'buildx',
            'build',
            '--progress=plain',
            '-t',
            image,
            '-f',
            targetDockerfilePath,
            contextPath,
            '--push',
            '--cache-from',
            'remote/cache/image:latest',
            '--cache-to',
            'remote/cache/image:latest',
        ].toString());
    });

    it('should return the correct args when cacheType is remote and cacheImageId is not provided', () => {
        const image = 'test/image:latest';
        const targetDockerfilePath = 'Dockerfile';
        const contextPath = '.';
        const result = getDockerBuildArgs(image, targetDockerfilePath, contextPath, "remote", "");

        const expectedCacheImageId = 'test/image:flow-docker-build-cache';
        expect(result.toString()).to.equal([
            'buildx',
            'build',
            '--progress=plain',
            '-t',
            image,
            '-f',
            targetDockerfilePath,
            contextPath,
            '--push',
            '--cache-from',
            expectedCacheImageId,
            '--cache-to',
            expectedCacheImageId,
        ].toString());
    });
});

describe('parseImage', () => {
    it('should replace both TIMESTAMP and DATETIME placeholders', () => {
        const input = 'tag_${TIMESTAMP}_${DATETIME}';
        expect(parseImage(input).length).to.equal(34);
    });

    it('should return the input string unchanged if no placeholders are present', () => {
        const input = 'tag';
        expect(parseImage(input)).to.equal(input);
    });
});